/*
 * Copyright (c) 2005-2009 Flamingo Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 
 *  o Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer. 
 *     
 *  o Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution. 
 *     
 *  o Neither the name of Flamingo Kirill Grouchnikov nor the names of 
 *    its contributors may be used to endorse or promote products derived 
 *    from this software without specific prior written permission. 
 *     
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */
package common;

import java.awt.Dimension;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.swing.JPanel;
import javax.swing.Scrollable;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import common.ui.BasicCommandButtonPanelUI;
import common.ui.CommandButtonPanelUI;

/**
 * Panel that hosts command buttons. Provides support for button groups, single
 * selection mode (for toggle command buttons), same icon state / dimension and
 * column-fill / row-fill layout.
 * 
 * <p>
 * Under the default {@link LayoutKind#ROW_FILL}, the buttons are laid out in
 * rows, never exceeding the available horizontal space. A vertical scroll bar
 * will kick in once there is not enough vertical space to show all the buttons.
 * The schematic below shows a row-fill command button panel:
 * </p>
 * 
 * <pre>
 * +-----------------------------+-+ 
 * |                             | |
 * | +----+ +----+ +----+ +----+ | |
 * | | 01 | | 02 | | 03 | | 04 | | |
 * | +----+ +----+ +----+ +----+ | |
 * |                             | |
 * | +----+ +----+ +----+ +----+ | |
 * | | 05 | | 06 | | 07 | | 07 | | |
 * | +----+ +----+ +----+ +----+ | |
 * |                             | |
 * | +----+ +----+ +----+ +----+ | |
 * | | 09 | | 10 | | 11 | | 12 | | |
 * | +----+ +----+ +----+ +----+ | |
 * |                             | |
 * | +----+ +----+ +----+ +----+ | |
 * | | 13 | | 14 | | 15 | | 16 | | |
 * +-----------------------------+-+
 * </pre>
 * 
 * <p>
 * Each row hosts four buttons, and the vertical scroll bar allows scrolling the
 * content down.
 * </p>
 * 
 * <p>
 * Under the {@link LayoutKind#COLUMN_FILL}, the buttons are laid out in
 * columns, never exceeding the available vertical space. A horizontal scroll
 * bar will kick in once there is not enough horizontal space to show all the
 * buttons. The schematic below shows a column-fill command button panel:
 * </p>
 * 
 * <pre>
 * +---------------------------------+ 
 * |                                 |
 * | +----+ +----+ +----+ +----+ +---|
 * | | 01 | | 04 | | 07 | | 10 | | 13|
 * | +----+ +----+ +----+ +----+ +---|
 * |                                 |
 * | +----+ +----+ +----+ +----+ +---|
 * | | 02 | | 05 | | 08 | | 11 | | 14|
 * | +----+ +----+ +----+ +----+ +---|
 * |                                 |
 * | +----+ +----+ +----+ +----+ +---|
 * | | 03 | | 06 | | 09 | | 12 | | 15|
 * | +----+ +----+ +----+ +----+ +---|
 * |                                 |
 * +---------------------------------+
 * +---------------------------------+
 * </pre>
 * 
 * <p>
 * Each column hosts three buttons, and the horizontal scroll bar allows
 * scrolling the content down.
 * </p>
 * 
 * @author Kirill Grouchnikov
 */
public class JCommandButtonPanel extends JPanel implements Scrollable {
    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    /**
     * @see #getUIClassID
     */
    public static final String uiClassID = "CommandButtonPanelUI";

    /**
     * List of titles for all button groups.
     * 
     * @see #getGroupCount()
     * @see #getGroupTitleAt(int)
     */
    protected List<String> groupTitles;

    /**
     * List of all button groups.
     * 
     * @see #getGroupCount()
     * @see #getGroupButtons(int)
     */
    protected List<List<AbstractCommandButton>> buttons;

    /**
     * Maximum number of columns for this panel. Relevant only when the layout
     * kind is {@link LayoutKind#ROW_FILL}.
     * 
     * @see #getMaxButtonColumns()
     * @see #setMaxButtonColumns(int)
     */
    protected int maxButtonColumns;

    /**
     * Maximum number of rows for this panel. Relevant only when the layout kind
     * is {@link LayoutKind#COLUMN_FILL}.
     * 
     * @see #getMaxButtonRows()
     * @see #setMaxButtonRows(int)
     */
    protected int maxButtonRows;

    /**
     * If <code>true</code>, the panel will show group labels.
     * 
     * @see #setToShowGroupLabels(boolean)
     * @see #isToShowGroupLabels()
     */
    protected boolean toShowGroupLabels;

    /**
     * Current icon dimension.
     */
    protected int currDimension;

    /**
     * Current icon state.
     */
    protected CommandButtonDisplayState currState;

    /**
     * Layout kind of this button panel.
     * 
     * @see #getLayoutKind()
     * @see #setLayoutKind(LayoutKind)
     */
    protected LayoutKind layoutKind;

    /**
     * Enumerates the available layout kinds.
     * 
     * @author Kirill Grouchnikov
     */
    public enum LayoutKind {
        /**
         * The buttons are layed out in rows respecting the available width.
         */
        ROW_FILL,

        /**
         * The buttons are layed out in columns respecting the available height.
         */
        COLUMN_FILL
    }

    /**
     * Creates a new panel.
     */
    protected JCommandButtonPanel() {
        this.buttons = new ArrayList<List<AbstractCommandButton>>();
        this.groupTitles = new ArrayList<String>();
        this.maxButtonColumns = -1;
        this.maxButtonRows = -1;
        this.toShowGroupLabels = true;
        this.setLayoutKind(LayoutKind.ROW_FILL);
    }

    /**
     * Creates a new panel.
     * 
     * @param startingDimension
     *            Initial dimension for buttons.
     */
    public JCommandButtonPanel(int startingDimension) {
        this();
        this.currDimension = startingDimension;
        this.currState = CommandButtonDisplayState.FIT_TO_ICON;
        this.updateUI();
    }

    /**
     * Creates a new panel.
     * 
     * @param startingState
     *            Initial state for buttons.
     */
    public JCommandButtonPanel(CommandButtonDisplayState startingState) {
        this();
        this.currDimension = -1;
        this.currState = startingState;
        this.updateUI();
    }

    /**
     * Adds a new button group at the specified index.
     * 
     * @param buttonGroupName
     *            Button group name.
     * @param groupIndex
     *            Button group index.
     * @see #addButtonGroup(String)
     * @see #removeButtonGroup(String)
     * @see #removeAllGroups()
     */
    public void addButtonGroup(String buttonGroupName, int groupIndex) {
        this.groupTitles.add(groupIndex, buttonGroupName);
        List<AbstractCommandButton> list = new ArrayList<AbstractCommandButton>();
        this.buttons.add(groupIndex, list);
        this.fireStateChanged();
    }

    /**
     * Adds a new button group after all the existing button groups.
     * 
     * @param buttonGroupName
     *            Button group name.
     * @see #addButtonGroup(String, int)
     * @see #removeButtonGroup(String)
     * @see #removeAllGroups()
     */
    public void addButtonGroup(String buttonGroupName) {
        this.addButtonGroup(buttonGroupName, this.groupTitles.size());
    }

    /**
     * Removes the specified button group.
     * 
     * @param buttonGroupName
     *            Name of the button group to remove.
     * @see #addButtonGroup(String)
     * @see #addButtonGroup(String, int)
     * @see #removeAllGroups()
     */
    public void removeButtonGroup(String buttonGroupName) {
        int groupIndex = this.groupTitles.indexOf(buttonGroupName);
        if (groupIndex < 0)
            return;
        this.groupTitles.remove(groupIndex);
        List<AbstractCommandButton> list = this.buttons.get(groupIndex);
        if (list != null) {
            for (AbstractCommandButton button : list) {
                this.remove(button);
            }
        }
        this.buttons.remove(groupIndex);
        this.fireStateChanged();
    }

    /**
     * Adds a new button to the specified button group.
     * 
     * @param commandButton
     *            Button to add.
     * @return Returns the index of the button on the specified group, or -1 if
     *         no such group exists.
     * @see #addButtonToGroup(String, AbstractCommandButton)
     * @see #addButtonToGroup(String, int, AbstractCommandButton)
     * @see #removeButtonFromGroup(String, int)
     */
    public int addButtonToLastGroup(AbstractCommandButton commandButton) {
        if (this.groupTitles.size() == 0)
            return -1;
        int groupIndex = this.groupTitles.size() - 1;
        commandButton.setDisplayState(this.currState);
        return this.addButtonToGroup(this.groupTitles.get(groupIndex),
                this.buttons.get(groupIndex).size(), commandButton);
    }

    /**
     * Adds a new button to the specified button group.
     * 
     * @param buttonGroupName
     *            Name of the button group.
     * @param commandButton
     *            Button to add.
     * @return Returns the index of the button on the specified group, or -1 if
     *         no such group exists.
     * @see #addButtonToGroup(String, int, AbstractCommandButton)
     * @see #addButtonToLastGroup(AbstractCommandButton)
     * @see #removeButtonFromGroup(String, int)
     */
    public int addButtonToGroup(String buttonGroupName,
            AbstractCommandButton commandButton) {
        int groupIndex = this.groupTitles.indexOf(buttonGroupName);
        if (groupIndex < 0)
            return -1;
        commandButton.setDisplayState(this.currState);
        return this.addButtonToGroup(buttonGroupName, this.buttons.get(
                groupIndex).size(), commandButton);
    }

    /**
     * Adds a new button to the specified button group.
     * 
     * @param buttonGroupName
     *            Name of the button group.
     * @param indexInGroup
     *            Index of the button in group.
     * @param commandButton
     *            Button to add.
     * @return Returns the index of the button on the specified group, or -1 if
     *         no such group exists.
     * @see #addButtonToGroup(String, int, AbstractCommandButton)
     * @see #addButtonToLastGroup(AbstractCommandButton)
     * @see #removeButtonFromGroup(String, int)
     */
    public int addButtonToGroup(String buttonGroupName, int indexInGroup,
            AbstractCommandButton commandButton) {
        int groupIndex = this.groupTitles.indexOf(buttonGroupName);
        if (groupIndex < 0)
            return -1;
        // commandButton.setState(ElementState.ORIG, true);
        this.add(commandButton);
        this.buttons.get(groupIndex).add(indexInGroup, commandButton);
        this.fireStateChanged();
        return indexInGroup;
    }

    /**
     * Removes the button at the specified index from the specified button
     * group.
     * 
     * @param buttonGroupName
     *            Name of the button group.
     * @param indexInGroup
     *            Index of the button to remove.
     * @see #addButtonToGroup(String, AbstractCommandButton)
     * @see #addButtonToGroup(String, int, AbstractCommandButton)
     * @see #addButtonToLastGroup(AbstractCommandButton)
     */
    public void removeButtonFromGroup(String buttonGroupName, int indexInGroup) {
        int groupIndex = this.groupTitles.indexOf(buttonGroupName);
        if (groupIndex < 0)
            return;

        AbstractCommandButton removed = this.buttons.get(groupIndex).remove(
                indexInGroup);
        this.remove(removed);
        this.fireStateChanged();
    }

    /**
     * Removes all the button groups and buttons from this panel.
     * 
     * @see #addButtonGroup(String, int)
     * @see #addButtonGroup(String)
     * @see #removeButtonGroup(String)
     * @see #removeButtonFromGroup(String, int)
     */
    public void removeAllGroups() {
        for (List<AbstractCommandButton> ljcb : this.buttons) {
            for (AbstractCommandButton jcb : ljcb) {
                this.remove(jcb);
            }
        }
        this.buttons.clear();
        this.groupTitles.clear();
        this.fireStateChanged();
    }

    /**
     * Returns the number of button groups in this panel.
     * 
     * @return Number of button groups in this panel.
     */
    public int getGroupCount() {
        if (this.groupTitles == null)
            return 0;
        return this.groupTitles.size();
    }

    /**
     * Returns the number of buttons in this panel.
     * 
     * @return Number of buttons in this panel.
     */
    public int getButtonCount() {
        int result = 0;
        for (List<AbstractCommandButton> ljcb : this.buttons) {
            result += ljcb.size();
        }
        return result;
    }

    /**
     * Returns the title of the button group at the specified index.
     * 
     * @param index
     *            Button group index.
     * @return Title of the button group at the specified index.
     */
    public String getGroupTitleAt(int index) {
        return this.groupTitles.get(index);
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.JPanel#updateUI()
     */
    @Override
    public void updateUI() {
        if (UIManager.get(getUIClassID()) != null) {
            setUI((CommandButtonPanelUI) UIManager.getUI(this));
        } else {
            setUI(BasicCommandButtonPanelUI.createUI(this));
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.JPanel#getUIClassID()
     */
    @Override
    public String getUIClassID() {
        return uiClassID;
    }

    /**
     * Sets the maximum button columns for this panel. When this panel is shown
     * and the layout kind is {@link LayoutKind#ROW_FILL}, it will have no more
     * than this number of buttons in each row. Fires a
     * <code>maxButtonColumns</code> property change event.
     * 
     * @param maxButtonColumns
     *            Maximum button columns for this panel.
     * @see #getMaxButtonColumns()
     * @see #setMaxButtonRows(int)
     */
    public void setMaxButtonColumns(int maxButtonColumns) {
        if (maxButtonColumns != this.maxButtonColumns) {
            int oldValue = this.maxButtonColumns;
            this.maxButtonColumns = maxButtonColumns;
            this.firePropertyChange("maxButtonColumns", oldValue,
                    this.maxButtonColumns);
        }
    }

    /**
     * Returns the maximum button columns for this panel. The return value is
     * relevant only when the layout kind is {@link LayoutKind#ROW_FILL}.
     * 
     * @return Maximum button columns for this panel.
     * @see #setMaxButtonColumns(int)
     * @see #getMaxButtonRows()
     */
    public int getMaxButtonColumns() {
        return this.maxButtonColumns;
    }

    /**
     * Sets the maximum button rows for this panel. When this panel is shown and
     * the layout kind is {@link LayoutKind#COLUMN_FILL}, it will have no more
     * than this number of buttons in each column. Fires a
     * <code>maxButtonRows</code> property change event.
     * 
     * @param maxButtonRows
     *            Maximum button rows for this panel.
     * @see #getMaxButtonRows()
     * @see #setMaxButtonColumns(int)
     */
    public void setMaxButtonRows(int maxButtonRows) {
        if (maxButtonRows != this.maxButtonRows) {
            int oldValue = this.maxButtonRows;
            this.maxButtonRows = maxButtonRows;
            this.firePropertyChange("maxButtonRows", oldValue,
                    this.maxButtonRows);
        }
    }

    /**
     * Returns the maximum button rows for this panel. The return value is
     * relevant only when the layout kind is {@link LayoutKind#COLUMN_FILL}.
     * 
     * @return Maximum button rows for this panel.
     * @see #setMaxButtonRows(int)
     * @see #getMaxButtonColumns()
     */
    public int getMaxButtonRows() {
        return this.maxButtonRows;
    }

    /**
     * Returns the list of all buttons in the specified button group.
     * 
     * @param groupIndex
     *            Group index.
     * @return Unmodifiable view on the list of all buttons in the specified
     *         button group.
     * @see #getGroupCount()
     */
    public List<AbstractCommandButton> getGroupButtons(int groupIndex) {
        return Collections.unmodifiableList(this.buttons.get(groupIndex));
    }

    /**
     * Sets indication whether button group labels should be shown. Fires a
     * <code>toShowGroupLabels</code> property change event.
     * 
     * @param toShowGroupLabels
     *            If <code>true</code>, this panel will show the labels of
     *            the button groups.
     * @see #isToShowGroupLabels()
     */
    public void setToShowGroupLabels(boolean toShowGroupLabels) {
        if ((layoutKind == LayoutKind.COLUMN_FILL) && toShowGroupLabels) {
            throw new IllegalArgumentException(
                    "Column fill layout is not supported when group labels are shown");
        }
        if (this.toShowGroupLabels != toShowGroupLabels) {
            boolean oldValue = this.toShowGroupLabels;
            this.toShowGroupLabels = toShowGroupLabels;
            this.firePropertyChange("toShowGroupLabels", oldValue,
                    this.toShowGroupLabels);
        }
    }

    /**
     * Returns indication whether button group labels should be shown.
     * 
     * @return If <code>true</code>, this panel shows the labels of the
     *         button groups, and <code>false</code> otherwise.
     * @see #setToShowGroupLabels(boolean)
     */
    public boolean isToShowGroupLabels() {
        return this.toShowGroupLabels;
    }

    /**
     * Sets the new dimension for the icons in this panel. The state for all the
     * icons is set to {@link CommandButtonDisplayState#FIT_TO_ICON}.
     * 
     * @param dimension
     *            New dimension for the icons in this panel.
     * @see #setIconState(CommandButtonDisplayState)
     */
    public void setIconDimension(int dimension) {
        this.currDimension = dimension;
        this.currState = CommandButtonDisplayState.FIT_TO_ICON;
        for (List<AbstractCommandButton> buttonList : this.buttons) {
            for (AbstractCommandButton button : buttonList) {
                button.updateCustomDimension(dimension);
            }
        }
        this.revalidate();
        this.doLayout();
        this.repaint();
    }

    /**
     * Sets the new state for the icons in this panel. The dimension for all the
     * icons is set to -1; this method should only be called with a state that
     * has an associated default size (like
     * {@link CommandButtonDisplayState#BIG},
     * {@link CommandButtonDisplayState#TILE},
     * {@link CommandButtonDisplayState#MEDIUM} and
     * {@link CommandButtonDisplayState#SMALL}).
     * 
     * @param state
     *            New state for the icons in this panel.
     * @see #setIconDimension(int)
     */
    public void setIconState(CommandButtonDisplayState state) {
        this.currDimension = -1;
        this.currState = state;
        for (List<AbstractCommandButton> ljrb : this.buttons) {
            for (AbstractCommandButton jrb : ljrb) {
                jrb.setDisplayState(state);
                jrb.revalidate();
                jrb.doLayout();
            }
        }
        this.revalidate();
        this.doLayout();
        this.repaint();
    }

    /**
     * Returns the layout kind of this panel.
     * 
     * @return Layout kind of this panel.
     * @see #setLayoutKind(LayoutKind)
     */
    public LayoutKind getLayoutKind() {
        return layoutKind;
    }

    /**
     * Sets the new layout kind for this panel. Fires a <code>layoutKind</code>
     * property change event.
     * 
     * @param layoutKind
     *            New layout kind for this panel.
     * @see #getLayoutKind()
     */
    public void setLayoutKind(LayoutKind layoutKind) {
        if (layoutKind == null)
            throw new IllegalArgumentException("Layout kind cannot be null");
        if ((layoutKind == LayoutKind.COLUMN_FILL)
                && this.isToShowGroupLabels()) {
            throw new IllegalArgumentException(
                    "Column fill layout is not supported when group labels are shown");
        }
        if (layoutKind != this.layoutKind) {
            LayoutKind old = this.layoutKind;
            this.layoutKind = layoutKind;
            this.firePropertyChange("layoutKind", old, this.layoutKind);
        }
    }

    /**
     * Adds the specified change listener to this button panel.
     * 
     * @param l
     *            Change listener to add.
     * @see #removeChangeListener(ChangeListener)
     */
    public void addChangeListener(ChangeListener l) {
        this.listenerList.add(ChangeListener.class, l);
    }

    /**
     * Removes the specified change listener from this button panel.
     * 
     * @param l
     *            Change listener to remove.
     * @see #addChangeListener(ChangeListener)
     */
    public void removeChangeListener(ChangeListener l) {
        this.listenerList.remove(ChangeListener.class, l);
    }

    /**
     * Notifies all registered listener that the state of this command button
     * panel has changed.
     */
    protected void fireStateChanged() {
        // Guaranteed to return a non-null array
        Object[] listeners = listenerList.getListenerList();
        // Process the listeners last to first, notifying
        // those that are interested in this event
        ChangeEvent event = new ChangeEvent(this);
        for (int i = listeners.length - 2; i >= 0; i -= 2) {
            if (listeners[i] == ChangeListener.class) {
                ((ChangeListener) listeners[i + 1]).stateChanged(event);
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.Scrollable#getPreferredScrollableViewportSize()
     */
    public Dimension getPreferredScrollableViewportSize() {
        return this.getPreferredSize();
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.Scrollable#getScrollableBlockIncrement(java.awt.Rectangle,
     *      int, int)
     */
    public int getScrollableBlockIncrement(Rectangle visibleRect,
            int orientation, int direction) {
        return 30;
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.Scrollable#getScrollableTracksViewportHeight()
     */
    public boolean getScrollableTracksViewportHeight() {
        return (this.layoutKind == LayoutKind.COLUMN_FILL);
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.Scrollable#getScrollableTracksViewportWidth()
     */
    public boolean getScrollableTracksViewportWidth() {
        return (this.layoutKind == LayoutKind.ROW_FILL);
    }

    /*
     * (non-Javadoc)
     * 
     * @see javax.swing.Scrollable#getScrollableUnitIncrement(java.awt.Rectangle,
     *      int, int)
     */
    public int getScrollableUnitIncrement(Rectangle visibleRect,
            int orientation, int direction) {
        return 10;
    }
}
